﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using RaytracerLib.MathLib;
using RaytracerLib.Samplers;
using System.Collections.Concurrent;
using NLog;
using RaytracerLib.Postprocessing.LightCorrector;
using TomanuExtensions;
using RaytracerLib.Rays;
using System.Threading;
using RaytracerLib.Resamplers;

namespace RaytracerLib.Renderers
{
    public abstract class Renderer
    {
        #region Tile
        protected class Tile
        {
            public Rectangle Rect;
            public Rectangle AffectedRect;
            public int X;
            public int Y;
            public bool Rendered = false;

            public override string ToString()
            {
                return String.Format("({0}, {1}), Rendered: {2}", X, Y, Rendered);
            }
        }
        #endregion

        private const int PIXELS_PER_TILE = 2500;
        private const int UPDATE_INTERVAL = 200;

        protected ConcurrentQueue<Rectangle> m_rects_to_update = new ConcurrentQueue<Rectangle>();
        private DateTime m_last_update = DateTime.Now;
        private List<Tile> m_tiles_to_render = new List<Tile>();
        private List<Tile> m_tiles_rendering = new List<Tile>();
        protected Tile[,] m_tiles_yx;
        private Scene m_scene;

        public event Action UpdateCallback;
        public event Func<bool> ShouldStop; 

        internal Film Film { get; private set; }
        internal Canvas Canvas { get; private set; }

        public Scene Scene 
        {
            get
            {
                return m_scene;
            }
            internal set
            {
                m_scene = value;

                Film = Scene.ActiveCamera.Film;
                Canvas = new Canvas(Film);
            }
        }

        public static Renderer Create(RendererType a_type)
        {
            if (a_type == RendererType.Backward)
                return new BackwardRenderer();
            if (a_type == RendererType.SamplesVisualizer)
                return new SamplesVisualizerRenderer();
            if (a_type == RendererType.SamplerPeriodogram)
                return new SamplesPeriodogramRenderer();
            else
                throw new NotImplementedException();
        }

        public Intersection RenderTestPixel(double a_x, double a_y)
        {
            if ((Scene.RenderOptions.DebugPixelX != -1) &&
                (Scene.RenderOptions.DebugPixelY != -1))
            {
                a_x = Scene.RenderOptions.DebugPixelX;
                a_y = Scene.RenderOptions.DebugPixelY;
            }
            else
            {
                a_x += 0.5;
                a_y += 0.5;
            }

            PrepareToRender();

            CameraRay r = Scene.ActiveCamera.CreateRay(Scene.ActiveCamera.PixelToUV(a_x, a_y));
            Intersection intersection = Scene.SpatialSubdivisioner.HitTest(null, r);

            Scene.RenderEnd(false);

            LogManager.GetLogger("MainForm").Info(
                "x: {0}; y: {1}; color: {2}", a_x, a_y, intersection.Color);

            #if DEBUG
            if (intersection.Exists)
                LogManager.GetLogger("MainForm").Info(intersection.DebugLogTree());
            #endif

            return intersection;
        }

        protected internal void PrepareToRender()
        {
            foreach (var phase in EnumExtensions.GetValues<RenderStartPhase>())
                Scene.RenderStart(phase);

            PrepareTiles(new Rectangle(0, 0, Scene.ActiveCamera.Film.Width,
               Scene.ActiveCamera.Film.Height));
        }

        private void TryRenderTile(Tile a_tile)
        {
            if (DoShouldStop())
                return;

            Thread.CurrentThread.Priority = Scene.RenderOptions.HighPriority ? 
                ThreadPriority.Highest : ThreadPriority.BelowNormal;

            lock (m_tiles_to_render)
            {
                m_tiles_to_render.Add(a_tile);
            }

            for (; ; )
            {
                Tile tile;

                lock (m_tiles_to_render)
                {
                    tile = m_tiles_to_render.FirstOrDefault(
                        t1 => m_tiles_rendering.All(
                            t2 => Rectangle.Intersect(t1.AffectedRect, t2.AffectedRect).IsEmpty));

                    if (tile == null)
                        break;

                    m_tiles_to_render.Remove(tile);
                    m_tiles_rendering.Add(tile);
                }

                RenderTile(tile);

                lock (m_tiles_to_render)
                {
                    m_tiles_rendering.Remove(tile);
                }
            }

            Thread.CurrentThread.Priority = ThreadPriority.Normal;
        }

        public virtual void Render()
        {
            PrepareToRender();

            Process.GetCurrentProcess().PriorityClass = Scene.RenderOptions.HighPriority ?
                ProcessPriorityClass.High : ProcessPriorityClass.BelowNormal;

            try
            {
                m_rects_to_update.Enqueue(Film.GetFilmRect());
                DoUpdateCallback();

                if (Scene.RenderOptions.Parallel)
                    Parallel.ForEach(m_tiles_yx.ToEnumerable(), (tile) => TryRenderTile(tile));
                else
                {
                    foreach (var tile in m_tiles_yx.ToEnumerable())
                    {
                        if (DoShouldStop())
                            break;

                        RenderTile(tile);
                    }
                }

                if (m_rects_to_update.Count != 0)
                    DoUpdateCallback();

                Canvas.PostprocessFinalize();

                m_rects_to_update.Enqueue(Film.GetFilmRect());
                DoUpdateCallback();

                Scene.RenderEnd(!DoShouldStop());
            }
            finally
            {
                Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.Normal;
            }
        }

        protected virtual void PrepareTiles(Rectangle a_rect)
        {
            double n = 1.0 * a_rect.Width * a_rect.Height / PIXELS_PER_TILE;
            int xn = (int)Math.Floor(Math.Sqrt(n * a_rect.Width / a_rect.Height));
            int yn = (int)Math.Floor(Math.Sqrt(n * a_rect.Height / a_rect.Width));

            if (xn == 0)
                xn++;
            if (yn == 0)
                yn++;

            m_tiles_yx = new Tile[yn, xn];

            for (int y = 0; y < yn; y++)
            {
                int y1 = (int)Math.Round((1.0 * a_rect.Height / yn) * y);
                int y2 = (int)Math.Round((1.0 * a_rect.Height / yn) * (y + 1));

                for (int x = 0; x < xn; x++)
                {
                    int x1 = (int)Math.Round((1.0 * a_rect.Width / xn) * x);
                    int x2 = (int)Math.Round((1.0 * a_rect.Width / xn) * (x + 1));

                    m_tiles_yx[y, x] = new Tile()
                    {
                        Rect = Rectangle.FromLTRB(x1, y1, x2, y2),
                        AffectedRect = Film.Resampler.GetAffectedRect(Rectangle.FromLTRB(x1, y1, x2, y2)), 
                        X = x,
                        Y = y
                    };
                }
            }
        }

        private void UpdateRenderedTile(Rectangle a_affected_rect_to_update)
        {
            if (UpdateCallback == null)
                return;

            m_rects_to_update.Enqueue(a_affected_rect_to_update);

            lock (m_rects_to_update)
            {
                double delta = (DateTime.Now - m_last_update).TotalMilliseconds;
                if ((m_rects_to_update.Count > 0) && (delta > UPDATE_INTERVAL))
                {
                    m_last_update = DateTime.Now;
                    DoUpdateCallback();
                }
            }
        }

        public virtual void UpdateBitmap(Bitmap a_bmp)
        {
            using (Graphics g = Graphics.FromImage(a_bmp))
            {
                Rectangle rect;
                while (m_rects_to_update.TryDequeue(out rect))
                {
                    Bitmap bmp = Canvas.GetPostprocessedBitmap(rect);
                    g.DrawImage(bmp, rect);
                }
            }
        }

        protected void DoUpdateCallback()
        {
            if (UpdateCallback != null)
                UpdateCallback();
        }

        protected bool DoShouldStop()
        {
            if (ShouldStop != null)
                return ShouldStop();
            else
                return false;
        }

        protected void TileRendered(Tile a_tile)
        {
            if (DoShouldStop())
                return;

            foreach (var r in SplitAffectedRect(a_tile))
            {
                Canvas.Resample(r);
                Canvas.Postprocess(r);
                UpdateRenderedTile(r);
            }

            lock (m_tiles_yx)
            {
                a_tile.Rendered = true;
            }

        }

        private Tile GetTile(int a_x, int a_y)
        {
            if (a_x < 0)
                return null;
            if (a_x >= m_tiles_yx.GetLength(1))
                return null;
            if (a_y < 0)
                return null;
            if (a_y >= m_tiles_yx.GetLength(0))
                return null;

            return m_tiles_yx[a_y, a_x];
        }

        protected IEnumerable<Rectangle> SplitAffectedRect(Tile a_tile)
        {
            Tile[,] tiles = new Tile[3, 3];

            for (int x = 0; x < 3; x++)
            {
                for (int y = 0; y < 3; y++)
                {
                    tiles[x, y] = GetTile(a_tile.X + x - 1, a_tile.Y + y - 1);
                }
            }

            List<Rectangle> list = new List<Rectangle>();

            Rectangle outer_rect = a_tile.AffectedRect;
            
            Rectangle inner_rect = Rectangle.FromLTRB(
                tiles[0, 1] != null ? tiles[0, 1].AffectedRect.Right : a_tile.Rect.Left,
                tiles[1, 0] != null ? tiles[1, 0].AffectedRect.Bottom : a_tile.Rect.Top,
                tiles[2, 1] != null ? tiles[2, 1].AffectedRect.Left : a_tile.Rect.Right,
                tiles[1, 2] != null ? tiles[1, 2].AffectedRect.Top : a_tile.Rect.Bottom);

            list.Add(inner_rect);

            if ((tiles[0, 1] != null) && (tiles[0, 1].Rendered) &&
                (tiles[0, 0] != null) && (tiles[0, 0].Rendered) &&
                (tiles[1, 0] != null) && (tiles[1, 0].Rendered))
            {
                list.Add(Rectangle.FromLTRB(outer_rect.Left, outer_rect.Top, inner_rect.Left, inner_rect.Top));
            }

            if ((tiles[1, 0] != null) && (tiles[1, 0].Rendered))
            {
                list.Add(Rectangle.FromLTRB(inner_rect.Left, outer_rect.Top, inner_rect.Right, inner_rect.Top));
            }

            if ((tiles[1, 0] != null) && (tiles[1, 0].Rendered) &&
                (tiles[2, 0] != null) && (tiles[2, 0].Rendered) &&
                (tiles[2, 1] != null) && (tiles[2, 1].Rendered))
            {
                list.Add(Rectangle.FromLTRB(inner_rect.Right, outer_rect.Top, outer_rect.Right, inner_rect.Top));
            }

            if ((tiles[2, 1] != null) && (tiles[2, 1].Rendered))
            {
                list.Add(Rectangle.FromLTRB(inner_rect.Right, inner_rect.Top, outer_rect.Right, inner_rect.Bottom));
            }

            if ((tiles[2, 1] != null) && (tiles[2, 1].Rendered) &&
                (tiles[2, 2] != null) && (tiles[2, 2].Rendered) &&
                (tiles[1, 2] != null) && (tiles[1, 2].Rendered))
            {
                list.Add(Rectangle.FromLTRB(inner_rect.Right, inner_rect.Bottom, outer_rect.Right, outer_rect.Bottom));
            }

            if ((tiles[1, 2] != null) && (tiles[1, 2].Rendered))
            {
                list.Add(Rectangle.FromLTRB(inner_rect.Left, inner_rect.Bottom, inner_rect.Right, outer_rect.Bottom));
            }

            if ((tiles[1, 2] != null) && (tiles[1, 2].Rendered) &&
                (tiles[0, 2] != null) && (tiles[0, 2].Rendered) &&
                (tiles[0, 1] != null) && (tiles[0, 1].Rendered))
            {
                list.Add(Rectangle.FromLTRB(outer_rect.Left, inner_rect.Bottom, inner_rect.Left, outer_rect.Bottom));
            }

            if ((tiles[0, 1] != null) && (tiles[0, 1].Rendered))
            {
                list.Add(Rectangle.FromLTRB(outer_rect.Left, inner_rect.Top, inner_rect.Left, inner_rect.Bottom));
            }

            Func<bool> try_merge = () =>
            {
                for (int i = 0; i < list.Count - 1; i++ )
                {
                    for (int j = i + 1; j < list.Count; j++)
                    {
                        Rectangle r3 = Rectangle.Union(list[i], list[j]);
                        int f3 = r3.Width * r3.Height;

                        int f1 = list[i].Width * list[i].Height;
                        int f2 = list[j].Width * list[j].Height;

                        if (f3 == f1 + f2)
                        {
                            list[i] = Rectangle.Union(list[i], list[j]);
                            list.RemoveAt(j);
                            return true;
                        }
                    }
                }

                return false;
            };

            for (; ; )
            {
                if (!try_merge())
                    break;
            }

            return list;
        }

        protected virtual void RenderTile(Tile a_rect)
        {
        }

        public abstract RendererType RendererType { get; }
    }
}